#pragma once

#include <unordered_map>
#include <list>
#include <GSLAM/core/TileManager.h>


namespace GSLAM {

inline bool operator == (const GSLAM::Point3i &p1, const GSLAM::Point3i &p2)
{
    return p1.x==p2.x&&p1.y==p2.y&&p1.z==p2.z;
}


class TileManagerCached : public GSLAM::TileManager
{
public:
    struct EqualKey
    {
        bool operator () (const GSLAM::Point3i &p1, const GSLAM::Point3i &p2) const
        {
            return p1.x==p2.x&&p1.y==p2.y&&p1.z==p2.z;
        }
    };

    struct HashKey
    {
        std::size_t operator()(const GSLAM::Point3i &pt) const
        {
            return ((std::hash<int>()(pt.x)
                ^ (std::hash<int>()(pt.y) << 1)) >> 1)
                ^ (std::hash<int>()(pt.z) << 1);
        }
    };

    struct Holder{
        Holder(const TilePtr& tile,TileManagerPtr* manager)
            :_tile(tile),_manager(manager){}

        ~Holder(){
            if(_manager&&*_manager&&_tile&&_tile->modified()){
                auto p=_tile->getTilePosition();
                (*_manager)->setTile(p.x,p.y,p.z,_tile);
            }
        }

        TilePtr _tile;
        TileManagerPtr* _manager;
    };

    typedef Point3i KeyType;
    typedef std::shared_ptr<Holder> HolderPtr;
    typedef std::pair<KeyType,HolderPtr> KeyValuePair;
    typedef std::list<KeyValuePair>      KeyValueList;
    typedef KeyValueList::iterator       KeyValueIt;
    typedef std::unordered_map<KeyType, KeyValueIt ,HashKey,EqualKey> TileHash;

    TileManagerCached(TileManagerPtr manager=TileManagerPtr(),
                      int cacheSize=1000)
        :_manager(manager),_cacheSize(cacheSize){
        if(manager){
            _projection=manager->getProjection();
            _area=manager->getTileArea();
        }
        else
            _projection=TileProjectionPtr(new MercatorProjection());
    }

    virtual ~TileManagerCached(){
        std::unique_lock<std::mutex> lock(_mutex);
        _hash.clear();
        _list.clear();
    }

    virtual std::string type()const{return "TileManagerCached";}

    virtual void  call(const std::string& command,void* arg=NULL)
    {
        if( command == "saveCache" )
        {
            for(auto it: _list)
            {
                TilePtr t = it.second->_tile;
                if(!t) continue;
                if( t->modified() ){
                    _manager->setTile(it.first.x, it.first.y, it.first.z, t);
                    t->setModified(false);
                }
            }
        }
        else if(_manager)
            _manager->call(command,arg);
    }

    virtual TilePtr getTile(int x,int y,int z)
    {
        std::unique_lock<std::mutex> lock(_mutex);

        KeyType key(x,y,z);
        auto it=_hash.find(key);

        if(it!=_hash.end()){
            _list.splice(_list.begin(),_list,it->second);
            return it->second->second->_tile;
        }

        if(!_manager) return TilePtr();

        // get tile from disk
        TilePtr tile=_manager->getTile(x,y,z);

        HolderPtr holder(new Holder(tile,&_manager));
        _list.push_front(KeyValuePair(key,holder));
        _hash[key]=_list.begin();

        if(_hash.size()>_cacheSize){
            auto last=_list.end();
            last--;
            _hash.erase(last->first);
            _list.pop_back();
        }

        return tile;
    }

    virtual bool setTile(int x,int y,int z, const TilePtr& tile)
    {
        std::unique_lock<std::mutex> lock(_mutex);

        TileArea area=getTileArea();
        if(z==area.getLevel()){
            area.max.x=std::max(area.max.x,x);area.max.y=std::max(area.max.y,y);
            area.min.x=std::min(area.min.x,x);area.min.y=std::min(area.min.y,y);
            setTileArea(area);
        }

        // find given tile in cache, if exist
        KeyType   key(x,y,z);
        auto it=_hash.find(key);
        if(it!=_hash.end()){
            _list.splice(_list.begin(),_list,it->second);
            HolderPtr holder=it->second->second;
            holder->_tile=tile;
            return true;
        }
        HolderPtr holder(new Holder(tile,&_manager));
        _list.push_front(KeyValuePair(key,holder));
        _hash[key]=_list.begin();

        if(_hash.size()>_cacheSize){
            auto last=_list.end();
            last--;
            _hash.erase(last->first);
            _list.pop_back();
        }
        return true;
    }


    virtual int maxZoomLevel()const{return _manager?_manager->maxZoomLevel():_area.level;}

    void setMaxZoomLevel(int level){
        TileArea area=getTileArea();
        area.setLevel(level);
        setTileArea(area);
    }

    virtual TileArea          getTileArea()const{return   _manager?_manager->getTileArea():_area;}
    virtual TileProjectionPtr getProjection()const{return _manager?_manager->getProjection():_projection;}

    virtual void setTileArea(TileArea area){
        if(_manager) _manager->setTileArea(area);
        else  _area=area;
    }

    virtual void setProjection(TileProjectionPtr projection){
        if(_manager) _manager->setProjection(projection);
        _projection=projection;
    }

    virtual bool save(const std::string &file){if(_manager) return _manager->save(file);}

protected:
    std::mutex              _mutex;
    int                     _cacheSize;
    TileHash                _hash;
    KeyValueList            _list;

    TileManagerPtr          _manager;
};
}
